<!DOCTYPE html>
<!--
Copyright 2017 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html">
<link rel="import" href="/tracing/metrics/metric_registry.html">
<link rel="import" href="/tracing/value/histogram.html">

<script>
'use strict';

// The |androidStartupMetric| produces metrics that (unless noted otherwise)
// start counting at the earliest moment the Chrome code on Android is
// executed. A few histograms are produced with the names as described below:
//
// * messageloop_start_time - Time till the message loop of the browser main
//   starts processing posted tasks (after having loaded the Profile)
//
// * experimental_navigation_start_time - This roughly corresponds to the time
//   the initial navigation network request is sent on the UI thread
//
// * experimental_network_request_start_time - Time until the first network
//   request starts in the netlog. The actual bytes will be sent to the network
//   slightly later: after disk cache responds. This metric can be used even for
//   netowrk loads that are served from the disk cache. Currently relies on
//   NetworkService running in the browser process.
//
// * navigation_commit_time - This is when the renderer has received the first
//   part of the response from the network after all redirects and started
//   loading the page. Recorded when the confirmation is received on the browser
//   UI thread.
//
// * first_contentful_paint_time - Time to the first 'contentful' paint of the
//   page loaded at startup
//
// * process_create_to_app_start_time - Time from process creation until the
//   application starts. The end time of this event will be the same as the
//   start time of messageloop_start_time.
//
// The metric also supports multiple browser restarts, in this case multiple
// samples would be added to the histograms above.
tr.exportTo('tr.metrics.sh', function() {
  const MESSAGE_LOOP_EVENT_NAME = 'Startup.BrowserMessageLoopStartTime';
  const NAVIGATION_EVENT_NAME = 'Navigation StartToCommit';
  const FIRST_CONTENTFUL_PAINT_EVENT_NAME = 'firstContentfulPaint';
  const APPLICATION_START_EVENT_NAME =
      'Startup.LoadTime.ProcessCreateToApplicationStart';
  const REQUEST_ALIVE = 'REQUEST_ALIVE';

  function isGetRequest(event) {
    if (event.args &&
        event.args.params &&
        event.args.params.method == 'GET') {
      return true;
    }
    return false;
  }

  function findRequestEventAfterNavigation(networkEvents, range) {
    let requestStart = range.max;
    let requestEvent = null;
    for (const event of networkEvents) {
      if (isGetRequest(event)) {
        for (const subSlice of event.subSlices) {
          if (subSlice.title === REQUEST_ALIVE &&
              range.min <= subSlice.start && subSlice.start < requestStart) {
            requestStart = subSlice.start;
            requestEvent = subSlice;
          }
        }
      }
    }
    return requestEvent;
  }

  function androidStartupMetric(histograms, model) {
    // Walk the browser slices, extract timestamps for the browser start,
    // message loop start.
    let messageLoopStartEvents = [];
    let applicationStartEvents = [];
    let navigationPairs = [];

    function organizeBrowserEventByTitle(event, process) {
      if (event.title === MESSAGE_LOOP_EVENT_NAME) {
        messageLoopStartEvents.push(event);
      } else if (event.title === APPLICATION_START_EVENT_NAME) {
        applicationStartEvents.push(event);
      } else if (event.title === NAVIGATION_EVENT_NAME) {
        const networkEvents =
            tr.e.chrome.EventFinderUtils.getNetworkEventsInRange(process,
                event.range);
        const requestEvent = findRequestEventAfterNavigation(
            networkEvents, event.range);
        navigationPairs.push({
          'nav': event,
          'req': requestEvent,
           });
      }
    }

    const chromeHelper =
        model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
    if (!chromeHelper) return;
    for (const helper of chromeHelper.browserHelpers) {
      for (const ev of helper.mainThread.asyncSliceGroup.childEvents()) {
        organizeBrowserEventByTitle(ev, helper.process);
      }
    }

    // Walk the renderer slices and extract the 'first contentful paint'
    // histogram samples.
    let firstContentfulPaintEvents = [];
    const rendererHelpers = chromeHelper.rendererHelpers;
    const pids = Object.keys(rendererHelpers);
    for (const rendererHelper of Object.values(chromeHelper.rendererHelpers)) {
      if (!rendererHelper.mainThread) continue;
      for (const ev of rendererHelper.mainThread.sliceGroup.childEvents()) {
        if (ev.title === FIRST_CONTENTFUL_PAINT_EVENT_NAME) {
          firstContentfulPaintEvents.push(ev);
          // There are usually several 'First Contentful Paint' events recorded
          // for each page load. Take only the first one per renderer.
          break;
        }
      }
    }

    // Fallback to scanning all processes if important events are not found.
    let totalBrowserStarts = messageLoopStartEvents.length;
    let totalFcpEvents = firstContentfulPaintEvents.length;
    if (totalBrowserStarts !== totalFcpEvents ||
        totalBrowserStarts !== navigationPairs.length ||
        totalBrowserStarts !== applicationStartEvents.length ||
        totalBrowserStarts === 0) {
      messageLoopStartEvents.length = 0;
      navigationPairs.length = 0;
      firstContentfulPaintEvents.length = 0;
      applicationStartEvents.length = 0;
      // Sometimes either the browser process or the renderer process does not
      // have the proper name attached. This often happens when both chrome
      // trace and systrace are merged. Other multi-process trickery, like
      // Perfetto, may also cause this.
      for (const proc of Object.values(model.processes)) {
        for (const ev of proc.getDescendantEvents()) {
          organizeBrowserEventByTitle(ev, proc);
        }
        for (const ev of proc.getDescendantEvents()) {
          if (ev.title === FIRST_CONTENTFUL_PAINT_EVENT_NAME) {
            firstContentfulPaintEvents.push(ev);
            break;
          }
        }
      }
      totalBrowserStarts = messageLoopStartEvents.length;
      totalFcpEvents = firstContentfulPaintEvents.length;
    }

    // Sort the events by start time.
    function orderEvents(event1, event2) {
      return event1.start - event2.start;
    }
    messageLoopStartEvents.sort(orderEvents);
    applicationStartEvents.sort(orderEvents);
    firstContentfulPaintEvents.sort(orderEvents);
    function orderPairs(pair1, pair2) {
      return pair1['nav'].start - pair2['nav'].start;
    }
    navigationPairs.sort(orderPairs);

    // Remove spurious navigation events. Only one 'Navigation StartToCommit'
    // event should remain for each browser session. When multiple events are
    // encountered for a browsing session, remove all except the first one. The
    // first navigation event looked more realistic for the story
    // maps_pwa:with_http_cache (http://crbug.com/1307774).
    let filteredNavigationPairs = [];
    let filteredRequestsFollowingNavigation = [];
    let navigation_index = 0;
    let messageloop_index = 0;
    do {
      // Set |browserStart| as the upper limit for the set of navigations to
      // select from.
      let browserStart = Number.POSITIVE_INFINITY;
      if (messageloop_index < messageLoopStartEvents.length) {
        browserStart = messageLoopStartEvents[messageloop_index].start;
      }

      // From navigation events preceding |browserStart| take the first one,
      // skip others.
      let navigationFound = false;
      for (; navigation_index < navigationPairs.length; navigation_index++) {
        // Each request event has to be at the same index as its corresponding
        // navigation event. Take either both of them or none.
        const navPair = navigationPairs[navigation_index];
        const navEvent = navPair['nav'];
        const requestEvent = navPair['req'];
        if (navEvent.start >= browserStart) {
          break;
        }
        if (!navigationFound) {
          if (messageloop_index === 0) {
            throw new Error('Found a navigation event before browser start');
          }
          filteredNavigationPairs.push(navPair);
          navigationFound = true;
        }
      }
      // Run the last iteration with |browserStart| as positive infinity.
    } while (messageloop_index++ <= messageLoopStartEvents.length);
    navigationPairs = filteredNavigationPairs;

    // Verify the number of FCP events to avoid pairing them to browser starts
    // incorrectly. Absence of navigation events does not cause FCP events to be
    // dropped.
    if (totalFcpEvents < totalBrowserStarts) {
      throw new Error('Found fewer FCP events (' + totalFcpEvents +
          ') than browser starts (' + totalBrowserStarts + ')');
    }

    // Group the relevant events with the corresponding browser starts and emit
    // the metrics.
    const messageLoopStartHistogram = histograms.createHistogram(
        'messageloop_start_time',
        tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, []);
    const navigationStartHistogram = histograms.createHistogram(
        'experimental_navigation_start_time',
        tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, []);
    const requestStartHistogram = histograms.createHistogram(
        'experimental_network_request_start_time',
        tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, []);
    const navigationCommitHistogram = histograms.createHistogram(
        'navigation_commit_time',
        tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, []);
    const firstContentfulPaintHistogram = histograms.createHistogram(
        'first_contentful_paint_time',
        tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, []);
    const applicationStartHistogram = histograms.createHistogram(
        'process_create_to_app_start_time',
        tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, []);
    // The earliest 2 browser starts are skipped to reduce measurement noise.
    // See http://crbug.com/891797.
    let navIndex = 0;
    let fcpIndex = 0;
    for (let loopStartIndex = 0; loopStartIndex < totalBrowserStarts;) {
      const loopStartEvent = messageLoopStartEvents[loopStartIndex];
      // There should be a corresponding application start event for each
      // message loop start event.
      const applicationStartEvent = applicationStartEvents[loopStartIndex];
      if (fcpIndex === totalFcpEvents) {
        break;
      }

      // Skip all events that appear before the next browser start.
      const navPair = navIndex < navigationPairs.length ?
        navigationPairs[navIndex] : null;
      const navEvent = navPair ? navPair['nav'] : null;
      if (navEvent && navEvent.start < loopStartEvent.start) {
        navIndex++;
        continue;
      }
      const fcpEvent = firstContentfulPaintEvents[fcpIndex];
      if (fcpEvent.start < loopStartEvent.start) {
        fcpIndex++;
        continue;
      }

      // The pair of matching events is found.
      loopStartIndex++;

      // Skip the two initial FCP events and (potentially missing) browser
      // starts.
      if (fcpIndex < 2) {
        continue;
      }

      // Record the histograms.
      messageLoopStartHistogram.addSample(loopStartEvent.duration,
          {events: new tr.v.d.RelatedEventSet([loopStartEvent])});
      if (navEvent) {
        navigationStartHistogram.addSample(
            navEvent.start - loopStartEvent.start,
            {events: new tr.v.d.RelatedEventSet([loopStartEvent, navEvent])});
        navigationCommitHistogram.addSample(
            navEvent.end - loopStartEvent.start,
            {events: new tr.v.d.RelatedEventSet([loopStartEvent, navEvent])});
        const requestEvent = navPair['req'];
        if (!requestEvent) {
          throw new Error('No network request after navigation');
        }
        requestStartHistogram.addSample(
            requestEvent.start - loopStartEvent.start,
            {events: new tr.v.d.RelatedEventSet([navEvent, requestEvent])});
      }
      firstContentfulPaintHistogram.addSample(
          fcpEvent.end - loopStartEvent.start,
          {events: new tr.v.d.RelatedEventSet([loopStartEvent, fcpEvent])});
      if (applicationStartEvent) {
        applicationStartHistogram.addSample(applicationStartEvent.duration,
            {events: new tr.v.d.RelatedEventSet([applicationStartEvent])});
      }
    }
  }

  tr.metrics.MetricRegistry.register(androidStartupMetric);

  return {
    androidStartupMetric,
  };
});
</script>
